{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example 3: Sensitivity analysis for a NetLogo model with SALib and Multiprocessing\n",
    "\n",
    "This is a short demo similar to example two but using the multiprocessing [Pool](https://docs.python.org/3.6/library/multiprocessing.html#module-multiprocessing.pool)\n",
    "All files used in the example are available from the pyNetLogo repository at https://github.com/quaquel/pyNetLogo.\n",
    "This code requires python3.\n",
    "\n",
    "For in depth discussion, please see example 2."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Running the experiments in parallel using a Process Pool\n",
    "\n",
    "There are multiple libraries available in the python ecosystem for performing tasks in parallel. One of the default libraries that ships with Python is [concurrent.futures](https://docs.python.org/3/library/concurrent.futures.html#module-concurrent.futures). This is in fact a high level interface around several other libraries. See the documentation for details. One of the libraries wrapped by concurrent.futures is multiprocessing. Below we use multiprocessing, anyone on python3.7 can use the either code below or use the ProcessPoolExecuturor from concurrent.futures (recommended). \n",
    "\n",
    "Here we are going to use the ProcessPoolExecutor, which uses the multiprocessing library. Parallelization is an advanced topic and the exact way in which it is to be done depends at least in part on the operating system one is using. It is recommended to carefully read the documentation provided by both concurrent.futures and mulitprocessing. This example is ran on a mac, linux is expected to be similar but Windows is likely to be slightly different"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from multiprocessing import Pool\n",
    "import os\n",
    "import pandas as pd\n",
    "\n",
    "import pyNetLogo\n",
    "from SALib.sample import saltelli\n",
    "\n",
    "def initializer(modelfile):\n",
    "    '''initialize a subprocess\n",
    "    \n",
    "    Parameters\n",
    "    ----------\n",
    "    modelfile : str\n",
    "    \n",
    "    '''\n",
    "    \n",
    "    # we need to set the instantiated netlogo\n",
    "    # link as a global so run_simulation can\n",
    "    # use it\n",
    "    global netlogo\n",
    "    \n",
    "    netlogo = pyNetLogo.NetLogoLink(gui=False)\n",
    "    netlogo.load_model(modelfile)\n",
    "\n",
    "\n",
    "def run_simulation(experiment):\n",
    "    '''run a netlogo model\n",
    "    \n",
    "    Parameters\n",
    "    ----------\n",
    "    experiments : dict\n",
    "    \n",
    "    '''\n",
    "    \n",
    "    #Set the input parameters\n",
    "    for key, value in experiment.items():\n",
    "        if key == 'random-seed':\n",
    "            #The NetLogo random seed requires a different syntax\n",
    "            netlogo.command('random-seed {}'.format(value))\n",
    "        else:\n",
    "            #Otherwise, assume the input parameters are global variables\n",
    "            netlogo.command('set {0} {1}'.format(key, value))\n",
    "\n",
    "    netlogo.command('setup')\n",
    "    # Run for 100 ticks and return the number of sheep and \n",
    "    # wolf agents at each time step\n",
    "    counts = netlogo.repeat_report(['count sheep','count wolves'], 100)    \n",
    "    \n",
    "    results = pd.Series([counts['count sheep'].values.mean(), \n",
    "                         counts['count wolves'].values.mean()], \n",
    "                         index=['Avg. sheep', 'Avg. wolves'])\n",
    "    return results\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    modelfile = os.path.abspath('./models/Wolf Sheep Predation_v6.nlogo')\n",
    "\n",
    "    problem = { \n",
    "      'num_vars': 6,\n",
    "      'names': ['random-seed',\n",
    "                'grass-regrowth-time',\n",
    "                'sheep-gain-from-food',\n",
    "                'wolf-gain-from-food',\n",
    "                'sheep-reproduce',\n",
    "                'wolf-reproduce'], \n",
    "      'bounds': [[1, 100000],\n",
    "                 [20., 40.], \n",
    "                 [2., 8.], \n",
    "                 [16., 32.],\n",
    "                 [2., 8.],\n",
    "                 [2., 8.]]\n",
    "    }\n",
    "\n",
    "    n = 1000\n",
    "    param_values = saltelli.sample(problem, n, \n",
    "                               calc_second_order=True)\n",
    "    \n",
    "    # cast the param_values to a dataframe to\n",
    "    # include the column labels\n",
    "    experiments = pd.DataFrame(param_values,\n",
    "                               columns=problem['names'])\n",
    "    \n",
    "    with Pool(4, initializer=initializer, initargs=(modelfile,)) as executor:\n",
    "        results = []\n",
    "        for entry in executor.map(run_simulation, experiments.to_dict('records')):\n",
    "            results.append(entry)\n",
    "        results = pd.DataFrame(results)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.6"
  },
  "latex_envs": {
   "LaTeX_envs_menu_present": true,
   "autocomplete": true,
   "bibliofile": "biblio.bib",
   "cite_by": "apalike",
   "current_citInitial": 1,
   "eqLabelWithNumbers": true,
   "eqNumInitial": 1,
   "hotkeys": {
    "equation": "Ctrl-E",
    "itemize": "Ctrl-I"
   },
   "labels_anchors": false,
   "latex_user_defs": false,
   "report_style_numbering": false,
   "user_envs_cfg": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
